package data.complex;

import java.util.HashSet;
import java.util.Set;

import data.base.Vector3;

public class VoxelSpace {
  private Vector3 origin;
  private double voxelSize;
  private Set<Triangle>[][][] voxels;
  private Set<Triangle> emptyVoxel = new HashSet<Triangle>();
  private Boundary vxboundary;
  private Boundary vxTestBoundary;
  private int triangleCount = 0;
  private int vertexCount = 0;
  
  @SuppressWarnings("unchecked")
  public VoxelSpace(Boundary boundary){
    origin = boundary.getMinorBounds();
    
    voxelSize = Math.cbrt(
        boundary.getDimension().getX() * 
        boundary.getDimension().getY() * 
        boundary.getDimension().getZ() / boundary.getVertexCount());
    
    vertexCount = boundary.getVertexCount();
    
    
    voxels = new Set[(int)(boundary.getDimension().getX() / voxelSize) + 2]
                     [(int)(boundary.getDimension().getY() / voxelSize) + 2]
                      [(int)(boundary.getDimension().getZ() / voxelSize) + 2];
  }
  
  public void registerTriangle(Triangle t){
    triangleCount++;
    Boundary b = new Boundary(t);
    Vector3 bDim = b.getDimension();
    int voxelsU = (int)(bDim.getX() / voxelSize) + 2;
    int voxelsV = (int)(bDim.getY() / voxelSize) + 2;
    int voxelsW = (int)(bDim.getZ() / voxelSize) + 2;
          
    for(int w = 0; w < voxelsW; w++){
      for(int v = 0; v < voxelsV; v++){
        for(int u = 0; u < voxelsU; u++){
          addTriangleAtIndex(t, toVoxelIndex(b.getMinorBounds(), u, v, w));
        }
      }
    }
  }
  
  private void addTriangleAtIndex(Triangle t, int[] index){
    if(voxels[index[0]][index[1]][index[2]] == null)
      voxels[index[0]][index[1]][index[2]] = new HashSet<Triangle>();

    voxels[index[0]][index[1]][index[2]].add(t);
  }
  
  public void calculateFirstTriangleIntersection(Ray r, double maxDistance, Triangle toDiscard){
    Vector3 is = r.calculateFirstBoundaryIntersection(getTestBoundary());
    if(is == null)
      return;
    
    int[] step = new int[3];
    int[] vxPos = new int[3];
    double[] fac = new double[3]; 
    double[] next = new double[3];

    for(int i = 0; i < 3; i++){
      step[i] = (int)Math.copySign(1, r.getDirection().get(i));
      vxPos[i] = (int)((is.get(i) - origin.get(i)) / voxelSize);
      fac[i] = voxelSize / Math.abs(r.getDirection().get(i));

      if(step[i] > 0){
        next[i] = (voxelSize - modulo((is.get(i) - origin.get(i)), voxelSize)) / Math.abs(r.getDirection().get(i)); // (voxelSize - modulo((is.get(i) - origin.get(i)), voxelSize)) / Math.abs(r.getDirection().get(i));
      }
      else{
        next[i] = modulo((is.get(i) - origin.get(i)), voxelSize) / Math.abs(r.getDirection().get(i));
      }
    }
    
    int minIndex;
    if(is != r){
      minIndex = getShortestAxis(next);
      next[minIndex] += fac[minIndex];
      vxPos[minIndex] += step[minIndex];
    }
    
    boolean intersectionFound = false;
    while(!intersectionFound){
      Set<Triangle> voxel = getVoxel(vxPos);
      
      if(voxel == null) 
        return;
      
      for(Triangle t : voxel){
        if(t != toDiscard){
          if(r.intersectWith(t, maxDistance)){
            maxDistance = r.getLength();
            if((int)((r.getIntersection().get(0) - origin.get(0)) / voxelSize) == vxPos[0] && 
               (int)((r.getIntersection().get(1) - origin.get(1)) / voxelSize) == vxPos[1] &&
               (int)((r.getIntersection().get(2) - origin.get(2)) / voxelSize) == vxPos[2]){
              intersectionFound = true;
            }
          }
        }
      }
      minIndex = getShortestAxis(next);
      next[minIndex] += fac[minIndex];
      vxPos[minIndex] += step[minIndex];
    }
  }
  
  private double modulo(double a, double b){
    return (a / b - Math.floor(a / b)) * b;
  }
  
  private int[] toVoxelIndex(Vector3 v, int xOff, int yOff, int zOff){
    int[] index = {
        (int)((v.getX() - origin.getX()) / voxelSize) + xOff, 
        (int)((v.getY() - origin.getY()) / voxelSize) + yOff, 
        (int)((v.getZ() - origin.getZ()) / voxelSize) + zOff};
    return index;
  }
  
  public int getShortestAxis(double[] coords){
    if(coords[0] < coords[1])
      if(coords[0] < coords[2])
        return 0;
      else
        return 2;
    else
      if(coords[1] < coords[2])
        return 1;
      else
        return 2;
  }
  
  public Set<Triangle> getVoxel(int[] index){
    if(index[0] < voxels.length && 
        index[1] < voxels[0].length && 
        index[2] < voxels[0][0].length &&
        index[0] >= 0 &&
        index[1] >= 0 &&
        index[2] >= 0){
       if(voxels[index[0]][index[1]][index[2]] == null){
         voxels[index[0]][index[1]][index[2]] = emptyVoxel;
       }
       return voxels[index[0]][index[1]][index[2]];
     }
     return null;
  }
  
  public Boundary getBoundary(){
    if(vxboundary == null){
      vxboundary = new Boundary(
          origin.getX(),
          origin.getY(),
          origin.getZ(),
          origin.getX() + voxelSize * voxels.length,
          origin.getY() + voxelSize * voxels[0].length,
          origin.getZ() + voxelSize * voxels[0][0].length);
    }
    return vxboundary;
  }
  
  private Boundary getTestBoundary(){
    if(vxTestBoundary == null){
      vxTestBoundary = getBoundary().getexpandedBoundary(voxelSize / 5);
    }
    return vxTestBoundary;
  }

  public int getTriangleCount() {
    return triangleCount;
  }

  public int getVertexCount() {
    return vertexCount;
  }
}
